/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_kit/zhiqim_git.htm
 *
 * ZhiqimGit is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.git.objer;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import org.zhiqim.git.GitConstants;
import org.zhiqim.git.GitI18n;
import org.zhiqim.git.GitZiper;
import org.zhiqim.git.objer.loader.DeltaOfsPackedObjectLoader;
import org.zhiqim.git.objer.loader.DeltaRefPackedObjectLoader;
import org.zhiqim.git.objer.loader.PackedObjectLoader;
import org.zhiqim.git.objer.loader.WholePackedObjectLoader;
import org.zhiqim.git.objer.objectid.ObjectId;
import org.zhiqim.git.util.GitStreams;

import org.zhiqim.kernel.util.Asserts;
import org.zhiqim.kernel.util.Bytes;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.RAFiles;
import org.zhiqim.kernel.util.mutables.MBuf;
import org.zhiqim.kernel.util.mutables.MInt;
import org.zhiqim.kernel.util.mutables.MLong;

/**
 * 基于Git索引版本=2的包对象，包括两个文件
 * 1、pack-${oid}.idx        索引文件
 * 2、pack-${oid}.pack       包文件
 *
 * @version v1.0.0 @author zouzhigang 2017-5-15 新建与整理
 */
public class PackFile implements GitConstants
{
    private PackIndex packIndex;
    private File packFile;

    public PackFile(PackIndex packIndex, File packFile) throws IOException
    {
        this.packIndex = packIndex;
        this.packFile = packFile;

        RandomAccessFile file = RAFiles.newFileR(packFile);
        Asserts.as(file != null?null:"读包文件时，读取文件失败");
        
        try
        {
            long len = file.length();
            Asserts.as(len >= 20?null:"读包文件时，包文件长度小于20");
            
            byte[] buf = new byte[20];

            //1.读出包头
            Files.read(file.getChannel(), 0, buf, 0, 12);
            Asserts.as(Bytes.match(buf, 0, PACK_SIGNATURE) == 4?null:"读包文件时，包文件格式错误");
            
            //2.读出版本号
            long version = Bytes.BU.getIntUnsigned(buf, 4);
            Asserts.as(version == 2?null:"读包文件时，不支持的版本号[版本：" + version + "]");

            //3.读出包数目
            long count = Bytes.BU.getIntUnsigned(buf, 8);
            Asserts.as(count == packIndex.count()?null:"读包文件时，包文件对象数目不匹配，[pack="+count+"][idx="+packIndex.count()+"]["+packFile+"]");
            
            //4.比较校验码
            Files.read(file.getChannel(), len - 20, buf, 0, 20);
            if (!Arrays.equals(buf, packIndex.getPackCheckSum()))
                throw Asserts.exception("包文件校验码不匹配，[pack=%s][idx=%s][%s]", ObjectId.parse(buf).getName(), ObjectId.parse(packIndex.getPackCheckSum()).getName(), packFile);
        }
        finally
        {
            RAFiles.close(file);
        }
    }
    
    /***********************************************************************************************/
    //文件操作
    /***********************************************************************************************/

    /**
     * 解压对象数据，指定对象偏移量和解压后的对象大小
     * 
     * @param offset        对象偏移量
     * @param size          对象大小
     * @return              解压后的对象数据
     * @throws IOException  异常
     */
    public final byte[] decompress(long offset, int size) throws DataFormatException, IOException
    {
        RandomAccessFile file = RAFiles.newFileR(packFile);
        Asserts.as(file != null?null:"解压对象时，读取文件失败");
        
        Inflater inf = GitZiper.inflater();
        
        try
        {
            byte[] dst = new byte[size];
            
            int count = 0, off = 0;
            MBuf buf = new MBuf();
            
            while (!inf.finished())
            {
                if (inf.needsInput())
                {
                    int num = RAFiles.readMaybe(file, offset+off, buf.value, 0, buf.len());
                    inf.setInput(buf.value, 0, num);
                    off += num;
                }
                
                count += inf.inflate(dst, count, dst.length - count);
            }
            
            if (count != size)
                throw new EOFException("解压对象时长度不一致：[" + offset + "]");
            
            return dst;
        }
        finally
        {
            GitZiper.close(inf);
            RAFiles.close(file);
        }
    }
    
    /**
     * 获取包对象加载器
     * 
     * @param id            对象编号
     * @return              包对象加载器
     * @throws IOException  异常
     */
    public PackedObjectLoader get(ObjectId id) throws IOException
    {
        long offset = packIndex.findOffset(id);
        return (offset > 0)?get(offset):null;
    }
    
    /**
     * 获取包对象加载器
     * 
     * @param offset        偏移量
     * @return              包对象加载器
     * @throws IOException  异常
     */
    public PackedObjectLoader get(long offset) throws IOException
    {
        RandomAccessFile file = RAFiles.newFileR(packFile);
        Asserts.as(file != null?null:"获取包对象加载器时，读取文件失败");
        
        try
        {
            long pos = offset;
            byte[] buf = new byte[20];
            RAFiles.readMaybe(file, pos, buf, 0, 20);
            
            //1.读取对象头信息（包括对象类型和对象展开后字节长度）
            MInt mType = new MInt();
            MLong mSize = new MLong();
            int count = GitStreams.readObjectHeader(buf, 0, mType, mSize);
            pos += count;
    
            switch (mType.value)
            {
            case OBJ_COMMIT:
            case OBJ_TREE:
            case OBJ_BLOB:
            case OBJ_TAG:
                return new WholePackedObjectLoader(this, pos, offset, mType.value, (int)mSize.value);
            case OBJ_OFS_DELTA:
            {
                RAFiles.readMaybe(file, pos, buf, 0, 20);
                int p = 0;
                int c = buf[p++] & 0xff;
                long ofs = c & 127;
                while ((c & 128) != 0)
                {
                    ofs += 1;
                    c = buf[p++] & 0xff;
                    ofs <<= 7;
                    ofs += (c & 127);
                }
                return new DeltaOfsPackedObjectLoader(this, pos + p, offset, (int) mSize.value, offset - ofs);
            }
            case OBJ_REF_DELTA:
            {
                RAFiles.readMaybe(file, pos, buf, 0, 20);
                return new DeltaRefPackedObjectLoader(this, pos + buf.length, offset, (int) mSize.value, ObjectId.parse(buf));
            }
            default:
                throw Asserts.exception(GitI18n.illegalObjectType, mType);
            }
        }
        finally
        {
            RAFiles.close(file);
        }
    }
}
